
Imports Microsoft.Ink
Imports System.Drawing

' One stroke in a million is a perfect set of lines.  The static functions in
' this class will perfect any stroke by approximating the points at which
' a major change in thought is found.  This is best used for replacing crude
' drawings of shapes with a better looking alternative.
Public Class PerfectStroke
    ' We need to know how hard to treat mistakes in the stroke object.  A
    ' lower value will be less strict, and a higher value more strict.
    Public AllowedVariation As Double = 1

    ' Client code calls this function when a stroke needs to be perfect.  The
    ' return value is a brand new perfect stroke object.
    Public Function ForcePerfect(ByVal Imperfect As Stroke) As Stroke
        ' We need to get a copy of the old, imperfect points.
        Dim Before As Point() = Imperfect.GetPoints()
        ' We need to create a new array with the perfected points.  To do
        ' this, we will call another private function in our class.
        Dim After As Point() = PerfectLines(Before)

        ' We create a new ink object to build the new, perfected stroke.
        Dim Ink As New Ink()
        ' Create the perfect stroke from the perfected points.
        Dim Perfect As Stroke = Ink.CreateStroke(After)

        ' Reset the color on the new, perfected stroke to the original color.
        Perfect.DrawingAttributes = Imperfect.DrawingAttributes
        ' Reset the drawing width on the new, perfected stroke.
        'Perfect.DrawingAttributes.Width = Imperfect.DrawingAttributes.Width

        ' Return the perfected stroke object to the client code.
        Return Perfect
    End Function

    ' This internal function is called when an array of points needs to be
    ' converted into its perfect form.
    Private Function PerfectLines(ByVal Points As Point()) As Point()
        ' Because we don't know how many points we will be creating, we will
        ' use an ArrayList to hold the perfected points.
        Dim Perfect As New ArrayList()
        ' To give us a starting point, we insert the first point of the
        ' passed imperfect stroke.
        Perfect.Add(Points(0))

        ' We will need to know the angle between two points.
        Dim Angle As Double
        ' Also, the change of that angle from the average will be needed.
        Dim Change As Double

        ' The total value of all angles encountered since the last perfect
        ' point.  This will be used to create the average angle value.
        Dim Total As Double
        ' The count of angles encountered since the last perfect point.  This
        ' will be used to create the average angle value.
        Dim Count As Double
        ' The actual average of all angles since the last perfect point.
        Dim Average As Double

        ' We need to loop through all the imperfect points, skipping the
        ' first angle because we already inserted it into the perfect list.
        Dim i As Integer = 0
        For i = 1 To Points.Length - 1
            ' Ask for the angle between the last point and the current one.
            Angle = FigureAngle(Points(i - 1), Points(i))
            ' If we have a value too small or non-existant, assume that it
            ' is going to be the average angle so far.
            If Double.IsNaN(Angle) Or Double.IsInfinity(Angle) Then Angle = Average
            ' If the average has not been defined yet, use the current angle.
            If Average = 0 Then Average = Angle

            ' The change between this angle and the average will be used to
            ' decide whether to include this point in the perfect list.
            Change = Math.Abs(Average - Angle)
            ' Check if the change between this angle and the average is more
            ' than one radian.
            If Change > Me.AllowedVariation Then
                ' Add the current point to the perfect list.
                Perfect.Add(Points(i))
                ' Reset all of our average counters to help us adjust to the
                ' new angle we just found.
                Total = 0 : Count = 0 : Average = 0
            End If

            ' Add the current angle to the total, and increment the count.
            Total += Angle : Count += 1
            ' Generate the average for the next round.
            Average = Total / Count
        Next

        ' To make sure we end where the user let go of the stroke, we will
        ' insert their last point.
        Perfect.Add(Points(Points.Length - 1))
        ' Convert the perfect list to a point array and return it to caller.
        Return Perfect.ToArray(GetType(Point))
    End Function

    ' This internal function is used to figure the angle of between two
    ' points on an X, Y axis.
    Private Function FigureAngle(ByVal First As Point, ByVal Second As Point) As Double
        ' We will need the difference in the X axis between the points.
        Dim X As Double = Double.Parse(Second.X.ToString()) - Double.Parse(First.X.ToString())
        ' We will also need the value of the hypotenuse of the triangle
        ' created by the intersection of the two points and the X axis.
        Dim Length As Double = Math.Sqrt(Math.Pow(Math.Abs(Second.X - First.X), 2) + Math.Pow(Math.Abs(Second.Y - First.Y), 2))
        ' Figure the angle using the arc-cosine function.
        Dim Angle As Double = Math.Acos(X / Length)

        ' Reture the angle (in radians) to the caller of our function.
        Return Angle
    End Function
End Class
